首先讓我們回顧一下可愛的StatelessWidget:
class Foo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
嗯沒什麼特別的,一個Widget subclass,複寫一個build函數來建立子樹,非常簡單直接。
接著再來看看今天的主角StatefulWidget:
class Bar extends StatefulWidget {
@override
_BarState createState() => _BarState();
}
class _BarState extends State<Bar> {
@override
Widget build(BuildContext context) {
return Container();
}
}
哇事情一下就複雜起來了!Bar有個createState()去建立_BarState(),而_BarState則是繼承了State<Bar>。到這裡勉強還說得通,但是_BarState裡面竟然有個跟StatelessWidget一樣的build函數,這是什麼巫術?為什麼build不是放在StatefulWidget裡面?這樣寫起來既複雜又不對稱,實在是讓人很不舒服。而且State類別聽起來就是單純存放資料的,現在竟然還要負責建立子樹,這不是很奇怪嗎?
這可以說是我最初在學Flutter時產生的第一個疑問,相信很多人第一眼看到StatefulWidget時心理也曾經出現過這樣的想法,今天就讓我們試著來釐清這個問題。
StatefulWidget時的彈性解釋這點之前,我們先來瞭解一下AnimatedWidget怎麼運作,這是它的部份原始碼:
abstract class AnimatedWidget extends StatefulWidget {
/// Override this method to build widgets that depend on the state of the
/// listenable (e.g., the current value of the animation).
@protected
Widget build(BuildContext context);
}
class _AnimatedState extends State<AnimatedWidget> {
@override
Widget build(BuildContext context) => widget.build(context);
}
你可以看到,AnimatedWidget繼承了StatefulWidget,並宣告一個abstract build(注意這不是從StatefulWidget繼承來的,而是AnimatedWidget自己宣告的)。接著_AnimatedState的build,透過widget回去呼叫那個abstract build。為什麼要這麼做?讓我們來看看AnimatedWidget的使用方式:
class MyRotatingWidget extends AnimatedWidget {
const MyRotatingWidget({
Key key,
AnimationController controller,
}) : super(key: key, listenable: controller,);
Animation<double> get _progress => listenable;
@override
Widget build(BuildContext context) {
return Transform.rotate(
angle: _progress.value,
child: FlutterLogo(),
);
}
}
動畫一定是有狀態_progress的,而狀態就須要StatefulWidget來管理,但如果我們唯一的狀態只有_progress,其它部份都是...stateless呢?我們還需要自己分別繼承StatefulWidget和State嗎?這就是AnimatedWidget幫你處理的事情,它幫你繼承去StatefulWidget和State並管理_progress。然後它宣告一個build函數給你覆寫,就好像你是在繼承StatelessWidget一樣。你不須要自己建立和管理狀態,也不須要知道AnimatedWidget背後其實有著StatefulWidget這樣的實作細節。
好,最後讓我們回到一開始的問題,如果我們把build搬回StatefulWidget會怎樣呢?
abstract class StatefulWidget extends Widget {
Widget build(BuildContext context, State state);
}
// option 1: extra build function
// result : confusing multiple build function
class AnimatedWidget extends StatefulWidget {
@protected
Widget build(BuildContext context);
}
// option 2: expose build function from StatefulWidget as-is
// result : unnecessarily expose [state], which should be an implementation detail
class AnimatedWidget extends StatefulWidget {
// do nothing
}
這時候StatefulWidget的build就必須接收一個State物件,畢竟你終究須要存放在裡面的狀態變數來建立子樹。而AnimatedWidget繼承StatefulWidget時就同時繼承了build(context, state),也就進一步把state這個實作細節暴露出去了。
AnimatedWidget只是其中一個例子,基本上任何時候,當我們想建立一個custom abstract widget來讓其它class繼承,幫child class管理狀態並隱藏起來時,如果有build(context, state)存在在StatefulWidget裡面,就會導致實作細節的暴露了。
Closure中隱含的this造成的bugclass MyButton extends StatefulWidget {
MyButton(this.color);
final Color color;
@override
Widget build(BuildContext context, State state) => FlatButton(
onPressed: () { print('color: $color'); },
);
}
假設今天有個MyButton繼承了我們新的StatefulWidget,其中有個屬性color。MyButton第一次被parent建立時被傳入Colors.blue,這時print裡的$color會是blue沒錯。但如果parent決定重新建立MyButton並傳入Colors.green,這時候print的$color卻依然會是blue。原因在於雖然MyButton是新的實例,但是FlatButton卻不會被重建,因為它沒有任何改變。也就是說這時onPressed被賦予的Closure () { print('color: $color'); } 也一樣是舊的Closure,而這裡面隱含取用的this也是舊的this,也就是舊的MyButton。
class MyButtonState extends State<MyButton> {
@override
Widget build(BuildContext context) => FlatButton(
onPressed: () { print('color: $widget.color'); },
);
}
相較之下,如果我們的build是在State裡面,因為我們是透過widget.color來取得color,而這裡的widget參照是會在StatefulWidget重新建立時被更新的,也就避免了上述的bug。
以上兩點是官方針對這個問題給出的回應,聽起來合理但好像又不是那麼有說服力。畢竟Stateless/StatefulWidget是Flutter最基本也最常被使用的元件,就為了這兩個感覺不怎麼嚴重的問題而讓整個設計複雜化,感覺是不是有點太小題大作了?說到底這一切問題的根源還是在於,一個被命名為State的類別負擔了太多不該是State的責任。如果一開始把它叫做ViewModel或Controller啥的,或許大家就不會那麼混亂了?